#!/usr/bin/python3
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t -*-

import os
import re
import glob
import json
import stat
import gzip
import mrget
import shutil
import pygit2
import gstage4
import portage
import pathlib
import platform
import fileinput
import subprocess
import strict_portage
import configparser
from datetime import datetime
from fm_util import Util
from fm_param import FmConst


class PkgWarehouse:

    def __init__(self):
        self.repoman = EbuildRepositories()
        self.layman = EbuildOverlays()

    def getPreEnableOverlays(self):
        ret = dict()
        modDir = os.path.join(FmConst.dataDir, "pre-enable")
        if os.path.exists(modDir):
            fullfnList = []
            fullfnList += list(glob.glob(os.path.join(modDir, "*.new_overlay")))
            for fullfn in fullfnList:
                cfg = configparser.ConfigParser()
                cfg.read(fullfn)
                name = cfg.get("main", "name", fallback=os.path.basename(fullfn).replace(".new_overlay", ""))
                url = cfg.get("main", "url", fallback=None)
                ret[name] = url
        return ret

    def getPreEnablePackages(self):
        ret = dict()
        modDir = os.path.join(FmConst.dataDir, "pre-enable")
        if os.path.exists(modDir):
            fullfnList = []
            fullfnList += list(glob.glob(os.path.join(modDir, "*.new_package")))
            for fullfn in fullfnList:
                cfg = configparser.ConfigParser()
                cfg.read(fullfn)
                name = cfg.get("main", "name")
                url = cfg.get("main", "url", fallback=None)
                package = cfg.get("main", "package")
                if name in ret:
                    if not (url is None or url == ret[name][0]):
                        print(fullfn, name, url)
                    assert url is None or url == ret[name][0]
                    ret[name][1].append(package)
                else:
                    ret[name] = (url, [package])
        return ret

    def getKeywordList(self):
        arch = platform.machine()
        if arch == "x86":
            return ["x86"]
        elif arch == "x86_64":
            return ["x86", "amd64"]
        else:
            assert False

    def getMaintPkgSets(self):
        ret = glob.glob(os.path.join(FmConst.portageCfgSetsDir, "maint-*"))
        return ["@" + os.path.basename(x) for x in ret]

    def checkHardwareUseFlags(self, machine):
        self._operateHardwareUseFlags(True, "30", "hardware", machine)

    def refreshHardwareUseFlags(self, machine):
        return self._operateHardwareUseFlags(False, "30", "hardware", machine)

    def checkLinguasUseFlags(self):
        self._operateLinguasUseFlags(True, "40", "linguas")

    def refreshLinguasUseFlags(self):
        return self._operateLinguasUseFlags(False, "40", "linguas")

    def pythonGetCurrentVersion(self):
        rc, ver = Util.cmdCallWithRetCode("eselect", "python", "show")
        if rc != 0:
            return None
        assert re.fullmatch(r"python[0-9]+\.[0-9]+", ver)
        return ver

    def pythonGetValidNewestVersion(self, curVer):
        out = subprocess.check_output(["eselect", "python", "list"], text=True)

        # get all versions
        verList = []
        for line in out.split("\n"):
            m = re.search(r"\s+(python[0-9]+\.[0-9]+)", line)
            if m is not None:
                verList.append(m.group(1))
        verList.sort(reverse=True)

        # check if there is a newer version with matches USE flags of all installed packages
        for ver in verList:
            if ver == curVer:
                return ver
            if len(self.pythonCheckVersionMatching(ver)) == 0:
                return ver

        # current version must be in version list
        assert False

    def pythonCheckVersionMatching(self, ver):
        targetsUsePrefix = "python_targets_"
        singleTargetUsePrefix = "python_single_target_"
        targetsUseFlag = targetsUsePrefix + ver.replace(".", "_")
        singleTargetUseFlag = singleTargetUsePrefix + ver.replace(".", "_")

        errMsgList = []
        for pkgNameVer in sorted(Util.portageGetInstalledPkgAtomList(FmConst.portageDbDir)):
            pkgName = Util.portagePkgNameVerToPkgName(pkgNameVer)
            ret = Util.portageGetPkgUseFlags(pkgName)
            if ret is None:
                errMsgList.append("Package \"%s\" is in abnormal status." % (pkgName))
                continue
            enabledUseList, allUseList = ret
            if "python" in enabledUseList or "python" not in allUseList:
                if any(x.startswith(targetsUsePrefix) for x in allUseList):
                    if targetsUseFlag not in allUseList:
                        errMsgList.append("Package \"%s\" has no USE flag \"%s\"." % (pkgName, targetsUseFlag))
                if any(x.startswith(singleTargetUsePrefix) for x in allUseList):
                    if singleTargetUseFlag not in allUseList:
                        errMsgList.append("Package \"%s\" has no USE flag \"%s\"." % (pkgName, singleTargetUseFlag))

        return errMsgList

    def pythonUpdateToVersion(self, ver):
        fullfn = "/etc/python-exec/python-exec.conf"
        assert os.path.exists(fullfn)
        pathlib.Path(fullfn).write_text(ver + "\n")

    def pythonCheckTargetUseFlags(self):
        self._operateTargetsUseFlags(True,
                                     "90", "python",
                                     self.__pythonGetDefaultTargetsUseFlag,
                                     self.__pythonCompareTargetsUseFlag,
                                     self.__pythonCheckMainPackageOfTargetUseFlag,
                                     self.__pythonGetSingleTargetUseFlag)

    def pythonRefreshTargetUseFlags(self):
        return self._operateTargetsUseFlags(False,
                                            "90", "python",
                                            self.__pythonGetDefaultTargetsUseFlag,
                                            self.__pythonCompareTargetsUseFlag,
                                            self.__pythonCheckMainPackageOfTargetUseFlag,
                                            self.__pythonGetSingleTargetUseFlag)

    def rubyGetCurrentVersion(self):
        # FIXME
        return None

    def rubyGetValidNewestVersion(self, curVer):
        return curVer

    def rubyCheckVersionMatching(self, ver):
        # FIXME
        return (None, [])

    def rubyUpdateToVersion(self, ver):
        assert False

    def rubyCheckTargetUseFlags(self):
        self._operateTargetsUseFlags(True,
                                     "90", "ruby",
                                     self.__rubyGetDefaultTargetsUseFlag,
                                     self.__rubyCompareDefaultTargetsUseFlag,
                                     self.__rubyCheckMainPackageOfTargetUseFlag,
                                     None)

    def rubyRefreshTargetUseFlags(self):
        return self._operateTargetsUseFlags(False,
                                            "90", "ruby",
                                            self.__rubyGetDefaultTargetsUseFlag,
                                            self.__rubyCompareDefaultTargetsUseFlag,
                                            self.__rubyCheckMainPackageOfTargetUseFlag,
                                            None)

    def wineGetCurrentVersion(self):
        rc, ver = Util.cmdCallWithRetCode("eselect", "wine", "show")
        if rc != 0:
            return None
        assert re.fullmatch(r"wine-.*-[0-9]+\.[0-9]+", ver)
        return ver

    def wineGetNewestVersion(self):
        out = subprocess.check_output(["eselect", "wine", "list"], text=True)

        # get all versions
        verList = []
        for line in out.split("\n"):
            m = re.search(r"\s+(wine-.*-[0-9]+\.[0-9]+)", line)
            if m is not None:
                verList.append(m.group(1))
        verList.sort(reverse=True)

        return verList[0]

    def checkUseFlagDeps(self):
        self._operateUseFlagDeps(True)

    def refreshUseFlagDeps(self):
        return self._operateUseFlagDeps(False)

    def checkMaintPkgSets(self, bAutoFix, errorCallback):
        setFnList = []
        useFnList = []

        def __genSetFileContentAndUseFileContent(fullfn):
            setFileBuf = ""
            useFileBuf = ""

            for line in pathlib.Path(fullfn).read_text().split("\n"):
                line = line.strip()
                if line == "":
                    continue

                m = re.fullmatch(r"(\S+?)(\[(\S+)\])?", line)
                if m is None:
                    # source file is invalid, not fixable
                    errorCallback("Invalid content for package set \"%s\"" % (fullfn))
                    return ("", "")

                setFileBuf += m.group(1) + "\n"

                if m.group(2) is not None:
                    if "-" in m.group(3):
                        # source file is invalid, not fixable
                        errorCallback("Invalid content for package set \"%s\"" % (fullfn))
                        return ("", "")
                    assert len(m.group(1)) < 40
                    useFileBuf += m.group(1) + " " * (40 - len(m.group(1))) + m.group(3).replace(",", " ") + "\n"

            if setFileBuf == "":
                # source file is invalid, not fixable
                errorCallback("Invalid content for package set \"%s\"" % (fullfn))
                return ("", "")

            return setFileBuf, useFileBuf

        def __ensureDstFileContent(fnType, fullfn, buf):
            if os.path.exists(fullfn):
                if pathlib.Path(fullfn).read_text() != buf:
                    if bAutoFix:
                        pathlib.Path(fullfn).write_text(buf)
                    else:
                        errorCallback("Invalid content for %s \"%s\"" % (fnType, fullfn))
            else:
                if bAutoFix:
                    os.makedirs(FmConst.portageCfgSetsDir, exist_ok=True)
                    pathlib.Path(fullfn).write_text(buf)
                else:
                    errorCallback("%s%s \"%s\" does not exist" % (fnType[0].upper(), fnType[1:], fullfn))

        def __removeRedundantFiles(fnType, fnList, dirPath, prefix):
            if os.path.exists(dirPath):
                for fn in os.listdir(dirPath):
                    if fn.startswith(prefix) and fn not in fnList:
                        fullfn = os.path.join(dirPath, fn)
                        if bAutoFix:
                            Util.forceDelete(fullfn)
                        else:
                            errorCallback("%s%s \"%s\" should not exist" % (fnType[0].upper(), fnType[1:], fullfn))
                if bAutoFix:
                    if len(os.listdir(dirPath)) == 0:
                        os.rmdir(dirPath)

        # add set files into /etc/portage/sets/maint-XXXX and /etc/portage/package.use/85-maint-XXXX
        for fullfn in glob.glob("/home/*/maint/*/@pkgset"):
            setBuf, useBuf = __genSetFileContentAndUseFileContent(fullfn)
            if setBuf == "":
                # source file is invalid, error already printed
                continue

            tlist = Util.realPathSplit(fullfn)

            if True:
                dstSetFn = "maint-" + tlist[2] + "-" + tlist[4]
                dstSetFullfn = os.path.join(FmConst.portageCfgSetsDir, dstSetFn)
                __ensureDstFileContent("package set file", dstSetFullfn, setBuf)
                setFnList.append(dstSetFn)

            if useBuf != "":
                dstUseFn = "85-maint-" + tlist[2] + "-" + tlist[4]
                dstUseFullfn = os.path.join(FmConst.portageCfgUseDir, dstUseFn)
                __ensureDstFileContent("USE flag flie", dstUseFullfn, useBuf)
                useFnList.append(dstUseFn)

        # remove redundant files
        __removeRedundantFiles("package set file", setFnList, FmConst.portageCfgSetsDir, "maint-")
        __removeRedundantFiles("USE flag flie", useFnList, FmConst.portageCfgUseDir, "85-maint-")

    def touchPortageTree(self):
        subprocess.run(["emerge", "-s", "non-exist-package"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

    def _operateHardwareUseFlags(self, checkOrRefresh, id, name, machine):
        usefn = os.path.join(FmConst.portageCfgUseDir, "%s-%s" % (id, name))
        cpuVendor, cpuModel = Util.hwcfgGetCpuVendorModel(machine)
        gpuCards = Util.hwcfgGetGraphicsCardComponents(machine)
        ret = {}

        def __generateUseFlagsFileContent(useFlagsMap):
            ret = ""
            for name, buf in useFlagsMap.items():
                ret += "## %s ##\n" % (name)
                ret += "\n"
                ret += buf
                ret += "\n"
                ret += "\n"
            return ret

        buf = ""
        if cpuVendor in ["intel", "amd"]:
            buf += "# ACPI must be used for x86 series architecture\n"
            buf += "*/* acpi\n"
            buf += "\n"
            buf += "# Position-Independent-Code is used for x86 series architecture\n"
            buf += "*/* pic\n"
            buf += "\n"
            buf += "# abi_x86_64 -> global support, abi_x86_32 -> per-package support\n"
            buf += "*/* ABI_X86: -* 64\n"
        else:
            assert False
        ret["general"] = buf

        buf = ""
        if cpuVendor in ["intel", "amd"]:
            buf += "# we enable all cpu flags for x86, applications will find out what is really supported by themselves\n"
            buf += "*/*               CPU_FLAGS_X86: -* aes avx avx2 avx512_4fmaps avx512_4vnniw avx512_bitalg avx512_fp16 avx512_vbmi2 avx512_vnni avx512_vpopcntdq avx512bw avx512cd avx512dq avx512er avx512f avx512ifma avx512pf avx512vbmi avx512vl f16c fma3 fma4 mmx mmxext popcnt sse sse2 sse3 sse4_1 sse4_2 sse4a ssse3\n"
            if cpuVendor == "amd" and cpuModel == "ryzen-2990wx":
                buf += "\n"
                buf += '# for cpu "%s %s"\n' % (cpuVendor, cpuModel)
                buf += "media-libs/opencv CPU_FLAGS_X86: -avx512f\n"
                buf += "media-libs/openimageio CPU_FLAGS_X86: -avx512f\n"
                buf += "dev-python/numpy CPU_FLAGS_X86: -avx512f -fma4 -xop\n"
        ret["cpu flags"] = buf

        buf = ""
        if True:
            buf += "# support all common graphics cards\n"                                                          # we have to do this because otherwise we can see nothing after changing graphics card
            buf += "*/* VIDEO_CARDS: -* amdgpu d3d12 intel lavapipe nouveau radeon radeonsi virgl vmware zink\n"    # FIXME: amdgpu disappeared?
            if any([x.get_vendor() == "amd" for x in gpuCards]):
                # note: not all GPUs are well suppported: https://bugs.gentoo.org/947583
                flagList = []
                if any([x.get_model() == "radeon7" for x in gpuCards]):
                    flagList.append("gfx906")
                if len(flagList) > 0:
                    buf += "\n"
                    buf += "# for AMD GPU in this machine\n"
                    buf += "*/* hip rocm\n"
                    buf += "*/* AMDGPU_TARGETS: -* %s\n" % (" ".join(flagList))
        ret["graphics"] = buf

        fnContent = __generateUseFlagsFileContent(ret)

        bModify = False
        if not os.path.exists(usefn):
            if checkOrRefresh:
                raise Exception("\"%s\" does not exist" % (usefn))
            else:
                bModify = True
        else:
            with open(usefn, "r") as f:
                if f.read() != fnContent:
                    if checkOrRefresh:
                        raise Exception("\"%s\" has invalid content" % (usefn))
                    else:
                        bModify = True
        if bModify:
            with open(usefn, "w") as f:
                f.write(fnContent)
        return bModify

    def _operateTargetsUseFlags(self, checkOrRefresh, id, name, getDefaultTargetsUseFlag, cmpTargetsUseFlag, checkMainPackageOfTargetUseFlag, getSingleTargetUseFlag):
        usefn = os.path.join(FmConst.portageCfgUseDir, "%s-%s-targets" % (id, name))

        # default use flag
        defaultUse = getDefaultTargetsUseFlag()
        if defaultUse is None:
            if os.path.exists(usefn):
                if checkOrRefresh:
                    raise Exception("\"%s\" should not exist" % (usefn))
                else:
                    Util.forceDelete(usefn)
                    return True
            else:
                return False
        else:
            ret, mainPackage = checkMainPackageOfTargetUseFlag(defaultUse)
            if not ret:
                raise Exception("main package \"%s\" for USE flag \"%s\" is masked" % (mainPackage, defaultUse))

            fnContent = ""
            fnContent += "# default version\n"
            if getSingleTargetUseFlag is None:
                fnContent += "*/* %s\n" % (defaultUse)
            else:
                fnContent += "*/* %s %s\n" % (defaultUse, getSingleTargetUseFlag(defaultUse))

            # use flag of higher versions
            if True:
                useSet = set()
                if True:
                    for repoName in self.repoman.getRepositoryList():
                        repoDir = self.repoman.getRepoDir(repoName)
                        fn = os.path.join(repoDir, "profiles", "desc", "%s_targets.desc" % (name))
                        if os.path.exists(fn):
                            useSet |= set(self.__getTargetsUseFlagList(fn))
                    for overlayName in self.layman.getOverlayList():
                        fn = os.path.join(self.layman.getOverlayDir(overlayName), "profiles", "desc", "%s_targets.desc" % (name))
                        if os.path.exists(fn):
                            useSet |= set(self.__getTargetsUseFlagList(fn))
                fnContent += "\n"
                fnContent += "# higher versions\n"
                if True:
                    line = ""
                    for u in sorted(list(useSet)):
                        if checkMainPackageOfTargetUseFlag(u)[0] and cmpTargetsUseFlag(useSet, u, defaultUse) > 0:
                            if getSingleTargetUseFlag is None:
                                line += " %s" % (u)
                            else:
                                line += " %s" % (u) + " -%s" % (getSingleTargetUseFlag(u))
                    if line != "":
                        fnContent += "*/*%s\n" % (line)
                    else:
                        fnContent += "\n"

            # operate configuration file
            bModify = False
            if not os.path.exists(usefn):
                if checkOrRefresh:
                    raise Exception("\"%s\" does not exist" % (usefn))
                else:
                    bModify = True
            else:
                with open(usefn, "r") as f:
                    if f.read() != fnContent:
                        if checkOrRefresh:
                            raise Exception("\"%s\" has invalid content" % (usefn))
                        else:
                            bModify = True
            if bModify:
                with open(usefn, "w") as f:
                    f.write(fnContent)
            return bModify

    def _operateLinguasUseFlags(self, checkOrRefresh, id, name):
        usefn = os.path.join(FmConst.portageCfgUseDir, "%s-%s" % (id, name))
        porttree = portage.db[portage.root]["porttree"]

        # get all languages
        useSet = set()
        for repoName in self.repoman.getRepositoryList():
            for pkgName in Util.repoGetEbuildDirList(self.repoman.getRepoDir(repoName)):
                for cpv in porttree.dbapi.match(pkgName):
                    for use in porttree.dbapi.aux_get(cpv, ["IUSE"])[0].split():
                        if use.startswith("l10n_"):
                            useSet.add(use[len("l10n_"):])
                        elif use.startswith("+l10n_"):
                            useSet.add(use[len("+l10n_"):])

        # trick: we keep "no" since "nb" and "no" conflict, see https://bugs.gentoo.org/775734
        if "nb" in useSet and "no" in useSet:
            useSet.remove("nb")

        # construct L10N line
        useList = sorted(list(useSet))
        fnContent = "*/*     L10N: %s" % (" ".join(useList))

        # file operation
        bModify = False
        if not os.path.exists(usefn):
            if checkOrRefresh:
                raise Exception("\"%s\" does not exist" % (usefn))
            else:
                bModify = True
        else:
            with open(usefn, "r") as f:
                if f.read() != fnContent:
                    if checkOrRefresh:
                        raise Exception("\"%s\" has invalid content" % (usefn))
                    else:
                        bModify = True
        if bModify:
            with open(usefn, "w") as f:
                f.write(fnContent)
        return bModify

    def _operateUseFlagDeps(self, checkOrRefresh):
        fnAutoUse = strict_portage.cfg.PackageUseMemberFile("99-autouse")
        if not fnAutoUse.exists():
            return []

        useMap = fnAutoUse.get_use_flag_mapping()
        pkgAffected = []

        # do the real work
        if "X" in useMap.get("x11-libs/cairo", set()):
            if "X" not in useMap.get("dev-cpp/cairomm", set()):
                useMap.setdefault("dev-cpp/cairomm", set()).add("X")
                pkgAffected.append("dev-cpp/cairomm")
            if "X" not in useMap.get("x11-libs/gtk+", set()):
                useMap.setdefault("x11-libs/gtk+", set()).add("X")
                pkgAffected.append("x11-libs/gtk+")
        if "X" in useMap.get("x11-libs/gtk+", set()):
            if "X" not in useMap.get("dev-cpp/gtkmm", set()):
                useMap.setdefault("dev-cpp/gtkmm", set().add("X"))
                pkgAffected.append("dev-cpp/gtkmm")

        if len(pkgAffected) == 0:
            return []

        if checkOrRefresh:
            raise Exception("USE flag dependencies is not fulfilled in \"%s\"" % (fnAutoUse.path))
        else:
            fnAutoUse.set_use_flag_mapping(useMap)
            return pkgAffected

    def __getTargetsUseFlagList(self, descFile):
        prefix = os.path.splitext(os.path.basename(descFile))[0]
        ret = []
        with open(descFile, "r") as f:
            for m in re.finditer("^(.*?)\\s+-\\s+.*", f.read(), re.M):
                if m.group(1).startswith("#"):
                    continue
                ret.append(prefix + "_" + m.group(1))
        return ret

    def __pythonGetDefaultTargetsUseFlag(self):
        rc, out = Util.cmdCallWithRetCode("eselect", "python", "show")
        if rc == 0:
            return "python_targets_" + out.replace(".", "_")
        else:
            return None

    def __pythonCompareTargetsUseFlag(self, useSet, a, b):
        assert a.startswith("python_targets_")
        assert b.startswith("python_targets_")
        a = a.replace("python_targets_", "")
        b = b.replace("python_targets_", "")

        if a.startswith("python") and b.startswith("python"):
            a = a.replace("python", "").replace("_", ".")
            b = b.replace("python", "").replace("_", ".")
            return Util.compareVersion(a, b)

        # we think "pypy" always be less than "pythonX.Y", so it won't be selected
        if a.startswith("pypy") and b.startswith("pypy"):
            return 0
        if a.startswith("python") and b.startswith("pypy"):
            return 1
        if a.startswith("pypy") and b.startswith("python"):
            return -1

        # we think "jython" always be less than "pythonX.Y", so it won't be selected
        if a.startswith("jython") or b.startswith("jython"):
            return 0
        if a.startswith("python") and b.startswith("jython"):
            return 1
        if a.startswith("jython") and b.startswith("python"):
            return -1

        assert False

    def __pythonCheckMainPackageOfTargetUseFlag(self, useFlag):
        assert useFlag.startswith("python_targets_")
        useFlag = useFlag.replace("python_targets_", "")

        if useFlag.startswith("python"):
            useFlag = useFlag.replace("python", "")
            slot = useFlag.replace("_", ".")
            pkgName = "dev-lang/python:%s" % (slot)
            return (Util.portageIsPkgInstallable(pkgName), pkgName)

        if useFlag.startswith("pypy"):
            return (False, None)

        if useFlag.startswith("jython"):
            return (False, None)

        assert False

    def __pythonGetSingleTargetUseFlag(self, useFlag):
        return useFlag.replace("python_targets_", "python_single_target_")

    def __rubyGetDefaultTargetsUseFlag(self):
        rc, out = Util.cmdCallWithRetCode("eselect", "ruby", "show")
        if rc == 0:
            m = re.search("ruby[0-9]+", out, re.M)
            return "ruby_targets_" + m.group(0)
        else:
            return None

    def __rubyCompareDefaultTargetsUseFlag(self, useSet, a, b):
        assert a.startswith("ruby_targets_")
        assert b.startswith("ruby_targets_")
        a = a.replace("ruby_targets_", "")
        b = b.replace("ruby_targets_", "")

        if a == "rbx" and b == "rbx":
            return 0

        # we think "rbx" always be less than "rubyXX", so it won't be selected
        if a.startswith("ruby") and b == "rbx":
            return 1
        if a == "rbx" and b.startswith("ruby"):
            return -1

        if a.startswith("ruby") and b.startswith("ruby"):
            a = a.replace("ruby", "")
            b = b.replace("ruby", "")
            return Util.compareVersion(a, b)

        assert False

    def __rubyCheckMainPackageOfTargetUseFlag(self, useFlag):
        assert useFlag.startswith("ruby_targets_")
        useFlag = useFlag.replace("ruby_targets_", "")

        if useFlag.startswith("ruby"):
            slot = useFlag[4] + "." + useFlag[5:]         # "ruby27" -> "2.7", "ruby210" -> "2.10"
            pkgName = "dev-lang/ruby:%s" % (slot)
            return (Util.portageIsPkgInstallable(pkgName), pkgName)

        if useFlag.startswith("rbx"):
            # FIXME: I don't know what rbx means...
            return (True, "")

        assert False


class EbuildRepositories:

    """
    When operating repositories, we think every existing repository is complete.
    The completeness check is done in checkRepository().
    """

    def __init__(self):
        self._repoInfoDict = {
            "gentoo": 5000,
            "guru": 4900,
            "mirrorshq": 4800,
            "bombyx-netutils": 4800,
            "your-own-os": 4800,
        }
        self._repoGitUrlDict = {
            "gentoo": "mirror://gentoo-portage-git",
            "guru": "mirror://github/gentoo/guru",                                   # FIXME: should get from overlay-db
            "mirrorshq": "https://gitee.com/mirrorshq/gentoo-overlay",
            "bombyx-netutils": "https://gitee.com/bombyx-netutils/gentoo-overlay",
            "your-own-os": "https://gitee.com/your-own-os/gentoo-overlay",
        }

    def getRepositoryList(self):
        return list(self._repoInfoDict.keys())

    def getRepoCfgReposFile(self, repoName):
        # returns /etc/portage/repos.conf/repo-XXXX.conf
        assert repoName in self._repoInfoDict
        return os.path.join(FmConst.portageCfgReposDir, "repo-%s.conf" % (repoName))

    def getRepoDir(self, repoName):
        assert repoName in self._repoInfoDict
        return os.path.join(FmConst.portageDataDir, "repo-%s" % (repoName))

    def isRepoExist(self, repoName):
        assert repoName in self._repoInfoDict
        return os.path.exists(self.getRepoCfgReposFile(repoName))

    def getRepoMetadata(self, repoName, key):
        # meta-data:
        #   1. repo-name: XXXX

        assert repoName in self._repoInfoDict
        assert self.isRepoExist(repoName)

        if key == "repo-name":
            return Util.repoGetRepoName(self.getRepoDir(repoName))
        else:
            assert False

    def checkRepository(self, repoName, bAutoFix=False, bAutoRemove=False):
        assert repoName in self._repoInfoDict
        assert not (bAutoFix and bAutoRemove)

        cfgFile = self.getRepoCfgReposFile(repoName)
        repoDir = self.getRepoDir(repoName)

        # check existence
        if not os.path.exists(cfgFile):
            if bAutoFix:
                self.createRepository(repoName)
            elif bAutoRemove:
                Util.forceDelete(repoDir)         # repoDir may exist
                return
            else:
                raise RepositoryCheckError("repository \"%s\" does not exist" % (repoName))

        # check cfgFile content
        if True:
            standardContent = self.__generateReposConfContent(repoName)
            if pathlib.Path(cfgFile).read_text() != standardContent:
                if bAutoFix:
                    pathlib.Path(cfgFile).write_text(standardContent)
                elif bAutoRemove:
                    Util.forceDelete(repoDir)     # repoDir may exist
                    Util.forceDelete(cfgFile)
                    return
                else:
                    raise RepositoryCheckError("file content of \"%s\" is invalid" % (cfgFile))

        # check repository directory existence
        if not os.path.exists(repoDir):
            if bAutoFix:
                self.createRepository(repoName)
            elif bAutoRemove:
                Util.forceDelete(cfgFile)
                return
            else:
                raise RepositoryCheckError("repository directory \"%s\" does not exist" % (repoDir))

        # check repository directory validity
        if not os.path.isdir(repoDir):
            if bAutoFix:
                Util.forceDelete(repoDir)
                self.createRepository(repoName)
            elif bAutoRemove:
                # we shouldn't delete repoDir here, it may belong to other program
                Util.forceDelete(cfgFile)
                return
            else:
                raise RepositoryCheckError("repository directory \"%s\" is invalid" % (repoDir))

        # check repository source url
        if repoName in self._repoGitUrlDict:
            repoObj = None
            try:
                repoObj = pygit2.Repository(repoDir)
            except pygit2.GitError:
                if bAutoFix:
                    Util.forceDelete(repoDir)
                    self.createRepository(repoName)
                elif bAutoRemove:
                    Util.forceDelete(repoDir)
                    Util.forceDelete(cfgFile)
                    return
                else:
                    raise RepositoryCheckError("repository directory \"%s\" has no git information" % (repoDir))

            if self._repoGitUrlDict[repoName].startswith("mirror://"):
                dstUrlList = mrget.target_urls(self._repoGitUrlDict[repoName])
            else:
                dstUrlList = [self._repoGitUrlDict[repoName]]
            if repoObj.remotes["origin"].url not in dstUrlList:
                if bAutoFix:
                    Util.forceDelete(repoDir)
                    self.createRepository(repoName)
                elif bAutoRemove:
                    Util.forceDelete(repoDir)
                    Util.forceDelete(cfgFile)
                    return
                else:
                    raise RepositoryCheckError("repository directory \"%s\" has invalid URL" % (repoDir))

        # check /etc/portage/make.profile for gentoo repository
        if repoName == "gentoo":
            # no way to auto-fix, we don't know which profile user want to use
            if not os.path.exists(FmConst.portageCfgMakeProfile):
                raise RepositoryCheckError("%s must exist" % (FmConst.portageCfgMakeProfile))
            else:
                tlist = Util.realPathSplit(os.path.realpath(FmConst.portageCfgMakeProfile))
                if not re.fullmatch("[0-9\\.]+", tlist[-1]):
                    raise RepositoryCheckError("%s must points to a vanilla profile (eg. default/linux/amd64/17.0)" % (FmConst.portageCfgMakeProfile))

    def createRepository(self, repoName):
        """Business exception should not be raise, but be printed as error message"""

        if repoName == "gentoo":
            self._repoGentooCreate(self.getRepoDir("gentoo"))
        elif repoName == "guru":
            self._repoGuruCreate(self.getRepoDir("guru"))
        else:
            if repoName in self._repoGitUrlDict:
                Util.gitPull(self.getRepoDir(repoName), reclone_on_failure=True, clone_url=self._repoGitUrlDict[repoName], depth=1)
                assert Util.repoGetThinManifests(self.getRepoDir(repoName))
            else:
                assert False

        with open(self.getRepoCfgReposFile(repoName), "w") as f:
            f.write(self.__generateReposConfContent(repoName))

    def syncRepository(self, repoName):
        """Business exception should not be raise, but be printed as error message"""

        if repoName == "gentoo":
            self._repoGentooSync(self.getRepoDir("gentoo"))
        elif repoName == "guru":
            self._repoGuruSync(self.getRepoDir("guru"))
        else:
            if repoName in self._repoGitUrlDict:
                Util.gitPull(self.getRepoDir(repoName), reclone_on_failure=True, clone_url=self._repoGitUrlDict[repoName], depth=1)
                assert Util.repoGetThinManifests(self.getRepoDir(repoName))
            else:
                assert False

        for fullfn in glob.glob(os.path.join(FmConst.portageCfgRepoPostsyncDir, "*")):
            if os.path.isfile(fullfn) and os.stat(fullfn).st_mode & stat.S_IXUSR:
                Util.cmdExec(fullfn, repoName, "", self.getRepoDir(repoName))

    def _repoGentooCreate(self, repoDir):
        os.makedirs(repoDir, exist_ok=True)
        self._repoGentooSync(repoDir)

    def _repoGentooSync(self, repoDir):
        cloneUrl = self._repoGitUrlDict["gentoo"]
        if cloneUrl.startswith("mirror://"):
            cloneUrl = mrget.target_urls(cloneUrl)[0]

        Util.gitPull(repoDir, reclone_on_failure=True, clone_url=cloneUrl, depth=1)
        assert Util.repoGetThinManifests(repoDir)

    def _repoGuruCreate(self, repoDir):
        os.makedirs(repoDir, exist_ok=True)
        self._repoGuruSync(repoDir)

    def _repoGuruSync(self, repoDir):
        cloneUrl = self._repoGitUrlDict["guru"]
        if cloneUrl.startswith("mirror://"):
            cloneUrl = mrget.target_urls(cloneUrl)[0]

        Util.gitPull(repoDir, reclone_on_failure=True, clone_url=cloneUrl, depth=1)
        assert Util.repoGetThinManifests(repoDir)

        # FIXME: ugly modification
        subprocess.check_call(["rm", "-rf", os.path.join(repoDir, "media-libs", "alvr")])                      # this package is outdated in guru, it is also contained in other overlay
        subprocess.check_call(["rm", "-rf", os.path.join(repoDir, "sci-misc", "llama-cpp")])                   # llama-cpp package in clx overlay is better
        subprocess.check_call(["rm", "-rf", os.path.join(repoDir, "app-accessibility", "whisper-cpp")])        # llama-cpp package in clx overlay is better

    def __generateReposConfContent(self, repoName):
        repoDir = self.getRepoDir(repoName)
        buf = ""
        buf += "[%s]\n" % (Util.repoGetRepoName(repoDir))
        buf += "auto-sync = no\n"
        buf += "priority = %d\n" % (self._repoInfoDict[repoName])
        buf += "location = %s\n" % (repoDir)
        return buf


class RepositoryCheckError(Exception):

    def __init__(self, message):
        self.message = message


class EbuildOverlays:

    """
    When operating overlays, we think overlay that exists is complete. Completeness check is done in basic post check stage.
    There're 3 types of overlays: staic, trusted, transient
    Overlay of type transient and trusted has vcs-type and url property
    """

    def __init__(self):
        self._repoman = EbuildRepositories()
        self._priority = "7000"

    def getOverlayList(self):
        if os.path.exists(FmConst.portageCfgReposDir):
            ret = glob.glob(os.path.join(FmConst.portageCfgReposDir, "overlay-*.conf"))
        else:
            ret = []
        ret = [re.fullmatch("overlay-(.*)\\.conf", os.path.basename(x)).group(1) for x in ret]
        ret.sort()
        return ret

    def getOverlayCfgReposFile(self, overlayName):
        # returns /etc/portage/repos.conf/overlay-XXXX.conf
        return os.path.join(FmConst.portageCfgReposDir, "overlay-%s.conf" % (overlayName))

    def getOverlayDir(self, overlayName):
        # returns /var/lib/portage/overlay-XXXX
        return os.path.join(FmConst.portageDataDir, "overlay-%s" % (overlayName))

    # deprecated
    def getOverlayFilesDir(self, overlayName):
        # returns /var/cache/portage/laymanfiles/XXXX
        return os.path.join(FmConst.laymanfilesDir, overlayName)

    def isOverlayExist(self, overlayName):
        return os.path.exists(self.getOverlayCfgReposFile(overlayName))

    def getOverlayType(self, overlayName):
        assert self.isOverlayExist(overlayName)
        buf = pathlib.Path(self.getOverlayCfgReposFile(overlayName)).read_text()
        priority, location, overlayType, vcsType, url, repoName = self._parseCfgReposFile(buf)
        assert overlayType in ["static", "trusted", "transient"]
        return overlayType

    def getOverlayVcsTypeAndUrl(self, overlayName):
        assert self.isOverlayExist(overlayName)
        buf = pathlib.Path(self.getOverlayCfgReposFile(overlayName)).read_text()
        priority, location, overlayType, vcsType, url, repoName = self._parseCfgReposFile(buf)
        assert overlayType in ["trusted", "transient"]
        assert vcsType is not None
        assert url is not None
        return (vcsType, url)

    def getOverlayMetadata(self, overlayName, key):
        # meta-data:
        #   1. repo-name: XXXX
        assert self.isOverlayExist(overlayName)

        if key == "repo-name":
            buf = pathlib.Path(self.getOverlayCfgReposFile(overlayName)).read_text()
            priority, location, overlayType, vcsType, url, repoName = self._parseCfgReposFile(buf)
            assert repoName is not None
            return repoName
        else:
            assert False

    def addTrustedOverlay(self, overlayName, overlayVcsType, overlayUrl):
        assert not self.isOverlayExist(overlayName)

        cfgFile = self.getOverlayCfgReposFile(overlayName)
        overlayDir = self.getOverlayDir(overlayName)

        self._createOverlaySourceDir(overlayName, overlayDir, overlayVcsType, overlayUrl)
        try:
            self._removeDuplicatePackage(overlayDir)
            with open(cfgFile, "w") as f:
                f.write(self._generateCfgReposFile(overlayName, overlayDir, "trusted", overlayVcsType, overlayUrl, Util.repoGetRepoName(overlayDir)))
        except:
            shutil.rmtree(overlayDir)       # keep overlay files directory
            raise

    def addTransientOverlay(self, overlayName, overlayVcsType, overlayUrl):
        assert not self.isOverlayExist(overlayName)

        cfgFile = self.getOverlayCfgReposFile(overlayName)
        overlayDir = self.getOverlayDir(overlayName)
        overlayFilesDir = self.getOverlayFilesDir(overlayName)

        self._createOverlaySourceDir(overlayName, overlayFilesDir, overlayVcsType, overlayUrl)
        try:
            self._createTransientOverlayDirFromOverlayFilesDir(overlayName, overlayDir, overlayFilesDir)
            with open(cfgFile, "w") as f:
                f.write(self._generateCfgReposFile(overlayName, overlayDir, "transient", overlayVcsType, overlayUrl, Util.repoGetRepoName(overlayDir)))
        except:
            shutil.rmtree(overlayDir)       # keep overlay files directory
            raise

    def removeOverlay(self, overlayName):
        assert self.isOverlayExist(overlayName)

        Util.forceDelete(self.getOverlayFilesDir(overlayName))
        Util.forceDelete(self.getOverlayDir(overlayName))
        Util.forceDelete(self.getOverlayCfgReposFile(overlayName))

    def modifyOverlayVcsTypeAndUrl(self, overlayName, overlayVcsType, overlayUrl):
        assert self.isOverlayExist(overlayName)

        cfgFile = self.getOverlayCfgReposFile(overlayName)
        overlayDir = self.getOverlayDir(overlayName)
        overlayFilesDir = self.getOverlayFilesDir(overlayName)

        overlayType = self._parseCfgReposFile(pathlib.Path(cfgFile).read_text())[2]
        if overlayType == "static":
            assert False
        elif overlayType == "trusted":
            Util.forceDelete(overlayDir)
            self._createOverlaySourceDir(overlayName, overlayDir, overlayVcsType, overlayUrl)
            self._removeDuplicatePackage(overlayDir)
            with open(cfgFile, "w") as f:
                f.write(self._generateCfgReposFile(overlayName, overlayDir, "trusted", overlayVcsType, overlayUrl, Util.repoGetRepoName(overlayDir)))
        elif overlayType == "transient":
            Util.forceDelete(overlayFilesDir)
            self._createOverlaySourceDir(overlayName, overlayFilesDir, overlayVcsType, overlayUrl)
            self._refreshTransientOverlayDir(overlayName, overlayDir, overlayFilesDir)
            with open(cfgFile, "w") as f:
                f.write(self._generateCfgReposFile(overlayName, overlayDir, "transient", overlayVcsType, overlayUrl, Util.repoGetRepoName(overlayDir)))
        else:
            assert False

    def checkOverlay(self, overlayName, bCheckContent, bAutoFix=False):
        assert self.isOverlayExist(overlayName)

        cfgFile = self.getOverlayCfgReposFile(overlayName)
        overlayDir = self.getOverlayDir(overlayName)
        overlayFilesDir = self.getOverlayFilesDir(overlayName)
        buf = pathlib.Path(cfgFile).read_text()
        priority, location, overlayType, vcsType, overlayUrl, repoName = self._parseCfgReposFile(buf)

        # check cfgFile
        if True:
            if repoName is None:
                raise OverlayCheckError("no \"repo-name\" in file \"%s\", strange?!" % (cfgFile))
            if overlayType is None:
                raise OverlayCheckError("no \"overlay-type\" in file \"%s\"" % (cfgFile))
            if overlayType not in ["static", "trusted", "transient"]:
                raise OverlayCheckError("invalid \"overlay-type\" in file \"%s\"" % (cfgFile))
            if overlayType in ["trusted", "transient"]:
                if vcsType is None:
                    raise OverlayCheckError("no \"sync-type\" in file \"%s\"" % (cfgFile))
                if vcsType not in ["git", "svn"]:
                    raise OverlayCheckError("invalid \"sync-type\" in file \"%s\"" % (cfgFile))
                if overlayUrl is None:
                    raise OverlayCheckError("no \"sync-uri\" in file \"%s\"" % (cfgFile))

            bBad = False

            if not bBad and priority is None:
                if not bAutoFix:
                    raise OverlayCheckError("no \"priority\" in file \"%s\"" % (cfgFile))
                bBad = True
            if not bBad and priority != self._priority:
                if not bAutoFix:
                    raise OverlayCheckError("invalid \"priority\" in file \"%s\"" % (cfgFile))
                bBad = True
            if not bBad and location is None:
                if not bAutoFix:
                    raise OverlayCheckError("no \"location\" in file \"%s\"" % (cfgFile))
                bBad = True
            if not bBad and location != overlayDir:
                if not bAutoFix:
                    raise OverlayCheckError("invalid \"location\" in file \"%s\"" % (cfgFile))
                bBad = True

            if bBad:
                pathlib.Path(cfgFile).write_text(self._generateCfgReposFile(overlayName, overlayDir, overlayType, vcsType, overlayUrl, repoName))

        # check overlay files directory
        if overlayType == "static":
            # overlay files directory should not exist
            if os.path.exists(overlayFilesDir):
                raise OverlayCheckError("\"%s\" should not have overlay files directory \"%s\"" % (overlayName, overlayFilesDir))
        elif overlayType == "trusted":
            # overlay files directory should not exist
            if os.path.exists(overlayFilesDir):
                if bAutoFix:
                    Util.forceDelete(overlayFilesDir)
                else:
                    raise OverlayCheckError("\"%s\" should not have overlay files directory \"%s\"" % (overlayName, overlayFilesDir))
        elif overlayType == "transient":
            # doesn't exist
            if not os.path.exists(overlayFilesDir):
                if bAutoFix:
                    self._createOverlaySourceDir(overlayName, overlayFilesDir, vcsType, overlayUrl)
                    self._createTransientOverlayDirFromOverlayFilesDir(overlayName, overlayDir, overlayFilesDir)
                else:
                    raise OverlayCheckError("overlay files directory \"%s\" does not exist" % (overlayFilesDir))
            # invalid
            if not os.path.isdir(overlayFilesDir):
                if bAutoFix:
                    Util.forceDelete(overlayFilesDir)
                    self._createOverlaySourceDir(overlayName, overlayFilesDir, vcsType, overlayUrl)
                    self._createTransientOverlayDirFromOverlayFilesDir(overlayName, overlayDir, overlayFilesDir)
                else:
                    raise OverlayCheckError("overlay files directory \"%s\" is invalid" % (overlayFilesDir))
            # source url is invalid
            while True:
                if vcsType == "git":
                    try:
                        repoObj = pygit2.Repository(overlayFilesDir)
                    except pygit2.GitError:
                        if bAutoFix:
                            Util.forceDelete(overlayFilesDir)
                            self._createOverlaySourceDir(overlayName, overlayFilesDir, vcsType, overlayUrl)
                            self._refreshTransientOverlayDir(overlayName, overlayDir, overlayFilesDir)
                            break
                        else:
                            raise RepositoryCheckError("overlay files directory \"%s\" has no git information" % (overlayFilesDir))
                    realUrl = repoObj.remotes["origin"].url
                elif vcsType == "svn":
                    realUrl = Util.svnGetUrl(overlayFilesDir)
                else:
                    assert False
                if realUrl != overlayUrl:
                    if bAutoFix:
                        Util.forceDelete(overlayFilesDir)
                        self._createOverlaySourceDir(overlayName, overlayFilesDir, vcsType, overlayUrl)
                        self._refreshTransientOverlayDir(overlayName, overlayDir, overlayFilesDir)
                        break
                    else:
                        raise RepositoryCheckError("URL in overlay files directory \"%s\" does not match URL \"%s\"" % (overlayFilesDir, overlayUrl))
                break
        else:
            assert False

        # check overlay directory
        if overlayType == "static":
            # doesn't exist
            if not os.path.exists(overlayDir):
                if bAutoFix:
                    self._createEmptyStaticOverlayDir(overlayName, overlayDir)
                else:
                    raise OverlayCheckError("overlay directory \"%s\" does not exist" % (overlayDir))
            # invalid
            if not os.path.isdir(overlayDir) or os.path.islink(overlayDir):
                if bAutoFix:
                    self._createEmptyStaticOverlayDir(overlayName, overlayDir)
                else:
                    raise OverlayCheckError("overlay directory \"%s\" is invalid" % (overlayDir))
        elif overlayType == "trusted":
            # doesn't exist
            if not os.path.exists(overlayDir):
                if bAutoFix:
                    self._createOverlaySourceDir(overlayName, overlayDir, vcsType, overlayUrl)
                    self._removeDuplicatePackage(overlayDir)
                else:
                    raise OverlayCheckError("overlay directory \"%s\" does not exist" % (overlayDir))
            # invalid
            if not os.path.isdir(overlayDir):
                if bAutoFix:
                    Util.forceDelete(overlayDir)
                    self._createOverlaySourceDir(overlayName, overlayDir, vcsType, overlayUrl)
                    self._removeDuplicatePackage(overlayDir)
                else:
                    raise OverlayCheckError("overlay directory \"%s\" is invalid" % (overlayDir))
            # source url is invalid
            while True:
                if vcsType == "git":
                    try:
                        repoObj = pygit2.Repository(overlayDir)
                    except pygit2.GitError:
                        if bAutoFix:
                            Util.forceDelete(overlayDir)
                            self._createOverlaySourceDir(overlayName, overlayDir, vcsType, overlayUrl)
                            self._removeDuplicatePackage(overlayDir)
                            break
                        else:
                            raise RepositoryCheckError("overlay directory \"%s\" has no git information" % (overlayDir))
                    realUrl = repoObj.remotes["origin"].url
                elif vcsType == "svn":
                    realUrl = Util.svnGetUrl(overlayDir)
                else:
                    assert False
                if realUrl != overlayUrl:
                    if bAutoFix:
                        Util.forceDelete(overlayDir)
                        self._createOverlaySourceDir(overlayName, overlayDir, vcsType, overlayUrl)
                        self._removeDuplicatePackage(overlayDir)
                        break
                    else:
                        raise RepositoryCheckError("URL in overlay directory \"%s\" does not match URL \"%s\"" % (overlayDir, overlayUrl))
                break
        elif overlayType == "transient":
            # doesn't exist
            if not os.path.exists(overlayDir):
                if bAutoFix:
                    self._createTransientOverlayDirFromOverlayFilesDir(overlayName, overlayDir, overlayFilesDir)
                else:
                    raise OverlayCheckError("overlay directory \"%s\" does not exist" % (overlayDir))
            # invalid
            if not os.path.isdir(overlayDir) or os.path.islink(overlayDir):
                if bAutoFix:
                    self._createTransientOverlayDirFromOverlayFilesDir(overlayName, overlayDir, overlayFilesDir)
                else:
                    raise OverlayCheckError("overlay directory \"%s\" is invalid" % (overlayDir))
        else:
            assert False

        # get repoName of overlay
        realRepoName = Util.repoGetRepoName(overlayDir)
        if realRepoName is None:
            raise OverlayCheckError("can not get repo-name of overlay \"%s\"" % (overlayName))

        # check cfgFile again
        if repoName != realRepoName:
            if bAutoFix:
                with open(cfgFile, "w") as f:
                    f.write(self._generateCfgReposFile(overlayName, overlayDir, overlayType, vcsType, overlayUrl, realRepoName))
            else:
                raise OverlayCheckError("invalid \"repo-name\" in \"%s\"" % (cfgFile))

        # check overlay directory again
        if bCheckContent:
            if overlayType == "static":
                pass
            elif overlayType == "trusted":
                masters = set(Util.repoGetMasters(overlayDir))
                masters -= set(self._repoman.getRepositoryList())
                if len(masters) > 0:
                    raise OverlayCheckError("overlay \"%s\" needs the following extra master(s): %s" % (overlayName, ", ".join(masters)))
            elif overlayType == "transient":
                # all packages must be the same as overlay files directory
                for d in Util.repoGetEbuildDirList(overlayDir):
                    srcEbuildDir = os.path.join(overlayFilesDir, d)
                    dstEbuildDir = os.path.join(overlayDir, d)
                    if not os.path.exists(srcEbuildDir):
                        if bAutoFix:
                            Util.forceDelete(dstEbuildDir)
                        else:
                            raise OverlayCheckError("package \"%s\" in overlay \"%s\" should not exist any more" % (d, overlayName))
                    if not Util.isTwoDirSame(srcEbuildDir, dstEbuildDir):
                        if bAutoFix:
                            Util.forceDelete(dstEbuildDir)
                            shutil.copytree(srcEbuildDir, dstEbuildDir)
                        else:
                            raise OverlayCheckError("package \"%s\" in overlay \"%s\" is corrupt" % (d, overlayName))
                # no empty category directory
                for d in Util.repoGetCategoryDirList(overlayDir):
                    fulld = os.path.join(overlayDir, d)
                    if os.listdir(fulld) == []:
                        if bAutoFix:
                            os.rmdir(fulld)
                        else:
                            raise OverlayCheckError("category directory \"%s\" in overlay \"%s\" is empty" % (d, overlayName))
            else:
                assert False

    def syncOverlay(self, overlayName):
        if not self.isOverlayExist(overlayName):
            raise Exception("overlay \"%s\" is not installed" % (overlayName))

        cfgFile = self.getOverlayCfgReposFile(overlayName)
        overlayDir = self.getOverlayDir(overlayName)
        overlayType = self.getOverlayType(overlayName)
        buf = pathlib.Path(cfgFile).read_text()
        priority, location, overlayType, vcsType, overlayUrl, repoName = self._parseCfgReposFile(buf)

        if overlayType == "static":
            # static overlay is mantained by other means
            pass
        elif overlayType == "trusted":
            if not Util.isUrlPrivate(overlayUrl) or Util.tryPrivateUrl(overlayUrl):
                self._syncOverlaySourceDir(overlayName, overlayDir, vcsType, overlayUrl)
                self._removeDuplicatePackage(overlayDir)
            else:
                print("Overlay not accessible, ignored.")
        elif overlayType == "transient":
            if not Util.isUrlPrivate(overlayUrl) or Util.tryPrivateUrl(overlayUrl):
                overlayFilesDir = self.getOverlayFilesDir(overlayName)
                self._syncOverlaySourceDir(overlayName, overlayFilesDir, vcsType, overlayUrl)
                self._refreshTransientOverlayDir(overlayName, overlayDir, overlayFilesDir)
            else:
                print("Overlay not accessible, ignored.")
        else:
            assert False

    def isOverlayPackageEnabled(self, overlayName, pkgName):
        assert self.isOverlayExist(overlayName)
        return os.path.isdir(os.path.join(self.getOverlayDir(overlayName), pkgName))

    def enableOverlayPackage(self, overlayName, pkgName, quiet=False):
        assert self.isOverlayExist(overlayName)
        assert self.getOverlayType(overlayName) == "transient"

        overlayDir = self.getOverlayDir(overlayName)
        overlayFilesDir = self.getOverlayFilesDir(overlayName)
        srcEbuildDir = os.path.join(overlayFilesDir, pkgName)
        dstEbuildDir = os.path.join(overlayDir, pkgName)

        if os.path.isdir(dstEbuildDir):
            return

        if not os.path.isdir(srcEbuildDir):
            raise Exception("package \"%s\" does not exist in overlay \"%s\"" % (pkgName, overlayName))
        if portage.db[portage.root]["porttree"].dbapi.match(pkgName) != []:
            raise Exception("package \"%s\" has already exist" % (pkgName))

        os.makedirs(os.path.dirname(dstEbuildDir), exist_ok=True)
        shutil.copytree(srcEbuildDir, dstEbuildDir)

        if not quiet:
            print("Notice: You need to enable any dependent package manually.")

    def disableOverlayPackage(self, overlayName, pkgName):
        assert self.isOverlayExist(overlayName)
        assert self.getOverlayType(overlayName) == "transient"

        overlayDir = self.getOverlayDir(overlayName)
        overlayFilesDir = self.getOverlayFilesDir(overlayName)

        if os.path.islink(overlayDir):
            raise Exception("overlay \"%s\" is a trusted overlay" % (overlayName))
        if not os.path.exists(overlayFilesDir):
            raise Exception("overlay \"%s\" is a static overlay" % (overlayName))
        if not os.path.isdir(os.path.join(overlayDir, pkgName)):
            raise Exception("package \"%s\" is not enabled" % (pkgName))

        Util.forceDelete(os.path.join(overlayDir, pkgName))
        Util.removeEmptyDir(os.path.join(overlayDir, pkgName.split("/")[0]))

    def _createOverlaySourceDir(self, overlayName, overlayFilesDir, vcsType, url):
        if vcsType == "git":
            # overlayFilesDir may already exist
            Util.gitPull(overlayFilesDir, reclone_on_failure=True, clone_url=url, depth=1)
            Util.repoSetThinManifests(overlayFilesDir, True)                          # with git, we can know file difference without full manifest
        elif vcsType == "svn":
            # overlayFilesDir may already exist
            Util.svnUpdate(overlayFilesDir, recheckout_on_failure=True, checkout_url=url)
            Util.repoSetThinManifests(overlayFilesDir, True)                          # with svn, we can know file difference without full manifest
        elif vcsType == "mercurial":
            # FIXME
            assert False
        elif vcsType == "rsync":
            # FIXME
            assert False
        else:
            assert False

        self.__patch(overlayName, overlayFilesDir)

    def _syncOverlaySourceDir(self, overlayName, overlayFilesDir, vcsType, url):
        if vcsType == "git":
            # FIXME: we think private URL is optional, how to do this? refer svn code beblow
            Util.gitPull(overlayFilesDir, reclone_on_failure=True, clone_url=url, depth=1)
            Util.repoSetThinManifests(overlayFilesDir, True)                          # with git, we can know file difference without full manifest
        elif vcsType == "svn":
            Util.svnUpdate(overlayFilesDir, recheckout_on_failure=True, checkout_url=url)
            Util.repoSetThinManifests(overlayFilesDir, True)                          # with svn, we can know file difference without full manifest
        else:
            assert False

        self.__patch(overlayName, overlayFilesDir)

    def _removeDuplicatePackage(self, overlayFilesDir):
        # get all packages in all repositories
        infoDict = dict()
        for repoName in self._repoman.getRepositoryList():
            repoDir = self._repoman.getRepoDir(repoName)
            infoDict[repoName] = set(Util.repoGetEbuildDirList(repoDir))

        # get to-be-removed packages that duplicates with repositories
        pkgRemoved = set()
        oDirInfo = set(Util.repoGetEbuildDirList(overlayFilesDir))
        for k, v in infoDict.items():
            for item in list(v & oDirInfo):
                pkgRemoved.add(item)

        # record to-be-removed packages
        if len(pkgRemoved) > 0:
            with open(os.path.join(overlayFilesDir, "packages.removed"), "w") as f:
                for item in pkgRemoved:
                    f.write(item + "\n")

        # do remove
        for item in pkgRemoved:
            Util.repoRemovePackageAndCategory(overlayFilesDir, item)
            print("Duplicate package \"%s\" is automatically removed." % (item))

    def _createEmptyStaticOverlayDir(self, overlayName, overlayDir):
        Util.forceDelete(overlayDir)
        os.mkdir(overlayDir)

        os.mkdir(os.path.join(overlayDir, "profiles"))
        with open(os.path.join(overlayDir, "profiles", "repo_name"), "w") as f:
            f.write(overlayName)

        os.mkdir(os.path.join(overlayDir, "metadata"))
        with open(os.path.join(overlayDir, "metadata", "layout.conf"), "w") as f:
            f.write("masters = gentoo\n")
            f.write("thin-manifests = true\n")

    def _createTransientOverlayDirFromOverlayFilesDir(self, overlayName, overlayDir, overlayFilesDir):
        Util.forceDelete(overlayDir)
        os.mkdir(overlayDir)

        # create profile directory
        srcProfileDir = os.path.join(overlayFilesDir, "profiles")
        profileDir = os.path.join(overlayDir, "profiles")
        if os.path.exists(srcProfileDir):
            Util.cmdCall("cp", "-r", srcProfileDir, profileDir)
            Util.forceDelete(os.path.join(profileDir, "profiles.desc"))
        else:
            os.mkdir(profileDir)
            layoutFn = os.path.join(overlayFilesDir, "metadata", "layout.conf")
            if os.path.exists(layoutFn):
                repoName = re.search("repo-name = (\\S+)", pathlib.Path(layoutFn).read_text(), re.M).group(1)
            else:
                repoName = overlayName
            with open(os.path.join(profileDir, "repo_name"), "w") as f:
                f.write(repoName)

        # create metadata directory
        srcMetaDataDir = os.path.join(overlayFilesDir, "metadata")
        metaDataDir = os.path.join(overlayDir, "metadata")
        if os.path.exists(srcMetaDataDir):
            shutil.copytree(srcMetaDataDir, metaDataDir)
        else:
            os.mkdir(metaDataDir)
            with open(os.path.join(metaDataDir, "layout.conf"), "w") as f:
                f.write("masters = gentoo")

        # create eclass directory
        srcEclassDir = os.path.join(overlayFilesDir, "eclass")
        eclassDir = os.path.join(overlayDir, "eclass")
        if os.path.exists(srcEclassDir):
            shutil.copytree(srcEclassDir, eclassDir)

        # ugly trick
        self.__overlayDirUglyTrick(overlayName, overlayDir)

    def _refreshTransientOverlayDir(self, overlayName, overlayDir, overlayFilesDir):
        profileDir = os.path.join(overlayDir, "profiles")
        Util.forceDelete(profileDir)

        # refresh profile directory
        srcProfileDir = os.path.join(overlayFilesDir, "profiles")
        if os.path.exists(srcProfileDir):
            Util.cmdCall("cp", "-r", srcProfileDir, profileDir)
            Util.forceDelete(os.path.join(profileDir, "profiles.desc"))
        else:
            os.mkdir(profileDir)
            layoutFn = os.path.join(overlayFilesDir, "metadata", "layout.conf")
            if os.path.exists(layoutFn):
                repoName = re.search("repo-name = (\\S+)", pathlib.Path(layoutFn).read_text(), re.M).group(1)
            else:
                repoName = overlayName
            with open(os.path.join(profileDir, "repo_name"), "w") as f:
                f.write(repoName)
        Util.touchFile(os.path.join(profileDir, "transient"))

        # refresh metadata directory
        srcMetaDataDir = os.path.join(overlayFilesDir, "metadata")
        metaDataDir = os.path.join(overlayDir, "metadata")
        if os.path.exists(srcMetaDataDir):
            Util.forceDelete(metaDataDir)
            shutil.copytree(srcMetaDataDir, metaDataDir)

        # refresh eclass directory
        srcEclassDir = os.path.join(overlayFilesDir, "eclass")
        dstEclassDir = os.path.join(overlayDir, "eclass")
        if os.path.exists(srcEclassDir):
            Util.forceDelete(dstEclassDir)
            shutil.copytree(srcEclassDir, dstEclassDir)
        else:
            Util.forceDelete(dstEclassDir)

        # refresh ebuild directories
        for d in Util.repoGetEbuildDirList(overlayDir):
            srcEbuildDir = os.path.join(overlayFilesDir, d)
            dstEbuildDir = os.path.join(overlayDir, d)
            Util.forceDelete(dstEbuildDir)
            if os.path.exists(srcEbuildDir):
                shutil.copytree(srcEbuildDir, dstEbuildDir)

        # remove empty category directories
        for d in Util.repoGetCategoryDirList(overlayDir):
            Util.removeEmptyDir(os.path.join(overlayDir, d))

        # ugly trick
        self.__overlayDirUglyTrick(overlayName, overlayDir)

    def _parseCfgReposFile(self, buf):
        m = re.search("^\\[(.*)\\]$", buf, re.M)
        if m is not None:
            innerRepoName = m.group(1)
        else:
            innerRepoName = None

        m = re.search("^priority *= *(.*)$", buf, re.M)
        if m is not None:
            priority = m.group(1)
        else:
            priority = None

        m = re.search("^location *= *(.*)$", buf, re.M)
        if m is not None:
            location = m.group(1)
        else:
            location = None

        m = re.search("^overlay-type *= *(.*)$", buf, re.M)
        if m is not None:
            overlayType = m.group(1)
        else:
            overlayType = None

        m = re.search("^sync-type *= *(.*)$", buf, re.M)
        if m is not None:
            vcsType = m.group(1)
        else:
            vcsType = None

        m = re.search("^sync-uri *= *(.*)$", buf, re.M)
        if m is not None:
            overlayUrl = m.group(1)
        else:
            overlayUrl = None

        return (priority, location, overlayType, vcsType, overlayUrl, innerRepoName)

    def _generateCfgReposFile(self, overlayName, overlayDir, overlayType, overlayVcsType, overlayUrl, innerRepoName):
        buf = ""
        buf += "[%s]\n" % (innerRepoName)
        buf += "auto-sync = no\n"
        buf += "priority = %s\n" % (self._priority)
        buf += "location = %s\n" % (overlayDir)
        buf += "overlay-type = %s\n" % (overlayType)
        if overlayVcsType is not None:
            buf += "sync-type = %s\n" % (overlayVcsType)
            buf += "sync-uri = %s\n" % (overlayUrl)
        return buf

    def __overlayDirUglyTrick(self, overlayName, overlayDir):
        # common trick
        with fileinput.FileInput(os.path.join(overlayDir, "metadata", "layout.conf"), inplace=True) as f:
            for line in f:
                if line.startswith("masters = "):
                    print("masters = gentoo")
                else:
                    print(line, end='')

        # ugly trick
        if overlayName == "unity":
            with fileinput.FileInput(os.path.join(overlayDir, "eclass", "ubuntu-versionator.eclass"), inplace=True) as f:
                for line in f:
                    print(line.replace("if [ -z \"${UNITY_BUILD_OK}\" ]; then", "if false; then"), end='')

    def __patch(self, repoNameOrOverlayName, repoDirOrOverlayDir):
        for fullfn in glob.glob(os.path.join(FmConst.portageCfgRepoPostsyncDir, "*")):
            if os.path.isfile(fullfn) and os.stat(fullfn).st_mode & stat.S_IXUSR:
                Util.cmdExec(fullfn, repoNameOrOverlayName, "", repoDirOrOverlayDir)


class OverlayCheckError(Exception):

    def __init__(self, message):
        self.message = message


class PrivateOverlayNotAccessiableError(Exception):
    pass


class CloudOverlayDb:

    """We expand overlay name "bgo" to ["bgo", "bgo-overlay", "bgo_overlay"]"""

    def __init__(self):
        self._name = "gentoo-overlays"
        self._fullfn = os.path.join(FmConst.cloudOverlayDbDir, self._name + ".json")

        # parse local cache
        try:
            self._data = Util.file2json(self._fullfn)
        except (FileNotFoundError, json.decoder.JSONDecodeError):
            self._data = None

    def update(self):
        db = gstage4.cloud_data.OverlayDb()

        localTm = None
        if os.path.exists(self._fullfn):
            tm = datetime.utcfromtimestamp(os.path.getmtime(self._fullfn))
            if db.get_last_modified_time() <= tm:
                if self._data is not None:
                    return
                try:
                    self._data = Util.file2json(self._fullfn)
                    localTm = tm
                    return
                except json.decoder.JSONDecodeError:
                    pass

        if localTm is None:
            self._data = self._modifyData(db.export_data())
            localTm = db.get_last_modified_time()
            Util.json2file(self._data, self._fullfn)
            os.utime(self._fullfn, (localTm.timestamp(), localTm.timestamp()))

        print("%s: %s" % (self._name, localTm.strftime("%Y%m%d%H%M%S")))

    def isUpdateComplete(self):
        return self._data is not None

    def hasOverlay(self, overlayName):
        assert self.isUpdateComplete()
        return self._getOverlayVcsTypeAndUrl(overlayName) is not None

    def getOverlayVcsTypeAndUrl(self, overlayName):
        # this function assumes that overlayName exists
        assert self.isUpdateComplete()
        vcsType, url = self._getOverlayVcsTypeAndUrl(overlayName)
        if url.startswith("mirror://"):
            url = mrget.target_urls(url)[0]
        return (vcsType, url)

    def _getOverlayVcsTypeAndUrl(self, overlayName):
        # expand overlay name
        if overlayName.endswith("-overlay") or overlayName.endswith("_overlay"):
            overlayNameList = [overlayName]
        else:
            overlayNameList = [overlayName, overlayName + "-overlay", overlayName + "_overlay"]

        # find overlay
        for overlayName in overlayNameList:
            if overlayName in self._data:
                return self._data[overlayName]
        return None

    def _modifyData(self, data):
        for overlayName in data.keys():
            # FIXME: sucks that I can't access this domain name
            if "repo.or.cz" in data[overlayName][1]:
                data[overlayName] = (data[overlayName][0], "https://github.com/gentoo-mirror/%s" % (overlayName))

            if data[overlayName][1].startswith("https://github.com/"):
                url = data[overlayName][1]
                url = url.replace("https://github.com/", "mirror://github/")
                # some github mirrors don't accept .git postfix?
                if url.endswith(".git"):
                    url = url[:-4]
                elif url.endswith(".git/"):
                    url = url[:-5]
                data[overlayName] = (data[overlayName][0], url)

        return data


class PkgMerger:

    def __init__(self, machine):
        self._fnAutoUse = strict_portage.cfg.PackageUseMemberFile("99-autouse")
        self._fnBreakCycle = strict_portage.cfg.PackageUseMemberFile("99-break-cycle")
        self._fnTmpAutoUse = strict_portage.cfg.PackageUseMemberFile("99-tmp-autouse")

        self._machine = machine
        self._pretendOutput = ""

    def doEmerge(self, cmd, autouse=True):
        assert cmd.startswith("-1 ")

        self._pretendOutput = ""

        with _FakeUsrSrcLinuxDirectory():
            self._fnTmpAutoUse.remove()

            # update USE flag
            while autouse:
                self._pretendEmerge("emerge -p %s" % (cmd))
                if not self._updateUseFlag():
                    break
                for pkgName in PkgWarehouse().refreshUseFlagDeps():
                    # we need to update packages that are modified by refreshUseFlagDeps() together
                    if Util.portageIsPkgInstalled(pkgName):
                        cmd += " %s" % (pkgName)

            # I found that removing dev-lang/perl instead of updating it can make the update process smooth
            perlRemoved = self._trickRemovePerl()

            # do merge
            subprocess.check_call("emerge %s" % (cmd), shell=True, env=self._prepareEnvVar())

            # do merge again
            if self._fnBreakCycle.exists():
                self._fnBreakCycle.remove()
                self._pretendEmerge("emerge -p %s" % (cmd))
                subprocess.check_call("emerge %s" % (cmd), shell=True, env=self._prepareEnvVar())

            # perl-cleaner
            if perlRemoved:
                self._pretendEmerge("perl-cleaner -p --all")
                subprocess.check_call(["perl-cleaner", "--all"], env=self._prepareEnvVar())

        # cleanup: sometimes emerge leaves /var/tmp/portage behind
        Util.forceDelete("/var/tmp/portage")

        # cleanup: merge "99-tmp-autouse" into "99-autouse"
        if self._fnTmpAutoUse.exists():
            self._fnAutoUse.merge_member_file(self._fnTmpAutoUse.name, remove_original=True)

    def reInstallPkg(self, pkgNameVer):
        self._pretendOutput = ""

        with _FakeUsrSrcLinuxDirectory():
            subprocess.run(["emerge", "-1", "=%s" % (pkgNameVer)], env=self._prepareEnvVar())

        # cleanup: sometimes emerge leaves /var/tmp/portage behind
        Util.forceDelete("/var/tmp/portage")

    def reInstallWorld(self):
        self._pretendOutput = ""

        maintPkgSets = []
        for fn in os.listdir(FmConst.portageCfgSetsDir):
            maintPkgSets.append("@" + fn)

        with _FakeUsrSrcLinuxDirectory():
            subprocess.run(["emerge", "-e", "@world", *maintPkgSets], env=self._prepareEnvVar())

        # cleanup: sometimes emerge leaves /var/tmp/portage behind
        Util.forceDelete("/var/tmp/portage")

    def _pretendEmerge(self, cmd):
        ret = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, text=True)
        self._pretendOutput = ret.stdout.rstrip()

    def _updateUseFlag(self):
        dictBreakCycle = {}
        dictAutoUse = {}

        bStart = False
        for line in self._pretendOutput.split("\n"):
            if not bStart:
                if line == "It might be possible to break this cycle":
                    bStart = True
                continue
            if line == "":
                bStart = False
                break
            m = re.fullmatch(r"- (\S+/\S+) \(Change USE: (\S+)\)", line)
            if m is not None:
                useFlagList = m.group(2).split(" ")
                useFlagList = [x[1:] if x.startswith("+") else x for x in useFlagList]      # +minimal -> minimal
                dictBreakCycle[m.group(1)] = useFlagList

        bStart = False
        for line in self._pretendOutput.split("\n"):
            if not bStart:
                if line == "The following USE changes are necessary to proceed:":
                    bStart = True
                continue
            if line == "":
                bStart = False
                break
            if line.startswith(" ") or line.startswith("#"):
                continue
            tlist = line.split(" ")
            pkgAtom = tlist[0]
            useList = tlist[1:]
            if True:
                useList2 = [x for x in useList if x.startswith("-")]
                if len(useList2) > 0:
                    # unable to process USE flag "-*"
                    raise Exception("it seems the following USE flag change is neccessary \"%s\", unable to proceed" % (" ".join(useList2)))
            dictAutoUse[pkgAtom] = useList

        if len(dictBreakCycle) > 0:
            pkgAtom, useFlagList = next(iter(dictBreakCycle.items()))
            newLine = "=%s %s\n" % (pkgAtom, " ".join(useFlagList))

            if self._fnBreakCycle.exists():
                buf = self._fnBreakCycle.get_content()
                if newLine in buf:
                    # duplicate hint found, it should not happen
                    # return False so that error can be reported to user
                    self._fnBreakCycle.remove()
                    return False
                buf += newLine
            else:
                buf = newLine

            self._fnBreakCycle.set_content(buf)

        if len(dictAutoUse) > 0:
            self._fnTmpAutoUse.merge_use_flag_mapping(dictAutoUse)

        return len(dictAutoUse) > 0 or len(dictBreakCycle) > 0

    def _trickRemovePerl(self):
        # [ebuild   U    ] dev-lang/perl-5.40.0
        m = re.search("^\\[ebuild\\s+.*U.*\\s+\\] dev-lang/perl-[0-9]+", self._pretendOutput, re.M)
        if m is not None:
            subprocess.check_call(["emerge", "-C", "dev-lang/perl"], stdout=subprocess.DEVNULL)
            return True
        else:
            return False

    def _prepareEnvVar(self):
        envVarDict = os.environ.copy()

        # determine parallelism parameters
        jobcountMake, loadavg = Util.portageDetectJobCountAndLoadAverage(self._machine)
        if True:
            def __match(pkgName):
                # [ebuild   R    ] net-libs/webkit-gtk-2.14.0
                return re.search("^\\[ebuild\\s+.*\\] %s-[0-9]+" % (pkgName), self._pretendOutput, re.M) is not None

            if __match("net-libs/webkit-gtk"):
                # each CPU consumes 1.6G memory on average when compiling
                jobcountMake = min(int(Util.getPhysicalMemorySize() / 1.6), jobcountMake)
            if __match("dev-qt/qtwebengine"):
                # each CPU consumes 2G memory on average when compiling (in fact 2G for GCC and 1.5G for Clang according to ebuild)
                jobcountMake = min(int(Util.getPhysicalMemorySize() / 2), jobcountMake)
            if __match("www-client/chromium"):
                # each CPU consumes 3G memory on average when compiling
                jobcountMake = min(int(Util.getPhysicalMemorySize() / 3), jobcountMake)
            if __match("sci-libs/composable-kernel"):
                # each CPU consumes 2G memory on average when compiling (we guess)
                jobcountMake = min(int(Util.getPhysicalMemorySize() / 2), jobcountMake)

            assert jobcountMake >= 1

        # MAKEOPTS
        value = strict_portage.cfg.MakeConf().get_var("MAKEOPTS")
        if True:
            m = re.fullmatch("(.*)\\B--jobs(=[0-9]+)?\\b(.*)", value)
            if m is not None:
                value = m.group(1) + m.group(3)
            m = re.search("(.*)\\B--load-average(=[0-9\\.]+)?\\b(.*)", value)
            if m is not None:
                value = m.group(1) + m.group(3)
            m = re.search("(.*)\\B-j([0-9]+)?\\b(.*)", value)
            if m is not None:
                value = m.group(1) + m.group(3)
            m = re.search("(.*)\\B-l([0-9]+)?\\b(.*)", value)
            if m is not None:
                value = m.group(1) + m.group(3)
        assert "MAKEOPTS" not in envVarDict
        envVarDict["MAKEOPTS"] = value + " --jobs=%d --load-average=%d -j%d -l%d" % (jobcountMake, loadavg, jobcountMake, loadavg)

        return envVarDict


class _FakeUsrSrcLinuxDirectory:

    def __init__(self, prefix=None):
        self.prefix = prefix if prefix is not None else ""

    def __enter__(self):
        if not self._checkUsrSrc():
            raise Exception("invalid /usr/src")

        bootDir = "%s/boot" % (self.prefix)
        dstDir = "%s/usr/src/linux" % (self.prefix)
        try:
            os.makedirs(dstDir, exist_ok=True)
            self._fillUsrSrcLinux(bootDir, dstDir)
            return self
        except Exception:
            self._close()
            raise

    def __exit__(self, type, value, traceback):
        assert self._checkUsrSrc()
        self._close()

    def _checkUsrSrc(self):
        if not os.path.exists("%s/usr/src" % (self.prefix)):
            return True
        else:
            for fn in os.listdir("%s/usr/src" % (self.prefix)):
                if fn == "linux":
                    continue
                if fn == ".keep":
                    continue
                return False
            return True

    def _fillUsrSrcLinux(self, bootDir, dstDir):
        flist = glob.glob(os.path.join(bootDir, "config-*"))
        flist = [x for x in flist if not x.endswith(".rules")]
        if flist != []:
            fn = flist[0]           # example: /mnt/gentoo/boot/config-x86_64-4.11.9
            shutil.copyfile(fn, os.path.join(dstDir, ".config"))
            ver = os.path.basename(fn).split("-")[2]
            version = ver.split(".")[0]
            patchlevel = ver.split(".")[1]
            sublevel = ver.split(".")[2]
        else:
            with gzip.open("/proc/config.gz", "rb") as f_in:
                with open(os.path.join(dstDir, ".config"), "wb") as f_out:
                    shutil.copyfileobj(f_in, f_out)
            version = Util.shellCall("uname -r | /usr/bin/cut -d '.' -f 1")
            patchlevel = Util.shellCall("uname -r | /usr/bin/cut -d '.' -f 2")
            sublevel = Util.shellCall("uname -r | /usr/bin/cut -d '.' -f 3")

        with open(os.path.join(dstDir, "Makefile"), "w") as f:
            f.write("# Faked by fpemud-refsystem to fool linux-info.eclass\n")
            f.write("\n")
            f.write("VERSION = %s\n" % (version))
            f.write("PATCHLEVEL = %s\n" % (patchlevel))
            f.write("SUBLEVEL = %s\n" % (sublevel))
            f.write("EXTRAVERSION = \n")

    def _close(self):
        if os.path.exists("%s/usr/src" % (self.prefix)):
            shutil.rmtree("%s/usr/src" % (self.prefix))
